<?php
/**
 * 专门用于判断麻将摸牌结果，打牌结果，胡牌，碰牌，杠牌的类
 * User: LiBing
 * Date: 2017/10/25
 * Time: 22:49
 */
    class MaJongLogic{

        /**
         * 检查能否碰牌的函数
         * @param $playerPai 玩家的手牌
         * @param $pai 打出来的具体牌
         * @return array 如果能碰，array($pai)/array()
         */
        function checkPeng($playerPai, $pai){
            $count = 0;
            for($i = 0; $i < count($playerPai); $i++){
                if($playerPai[$i] == $pai){
                    $count++;
                }
            }
            if($count >= 2){
                return array($pai);
            }
            else{
                return array();
            }
        }

        /**
         * 检查打牌能否杠牌的函数
         * @param $playerPai  玩家手牌
         * @param $pai 打的牌
         * @return bool  true/false
         */
        function checkDaPaiGang($playerPai, $pai){
            array_push($playerPai, $pai);
            sort($playerPai);
            $pai_count = array_count_values($playerPai);
            foreach($pai_count as $key => $value){
                if($value == 4 && $pai == $key){
                    return array($pai);
                }
            }
            return array();
        }

        /**
         * 检查摸牌后能否杠牌的函数
         * @param $playerPai 玩家的手牌
         * @param $playerPeng 玩家碰过的牌
         * @param $pai 打出来的具体牌
         * @return 如果能杠，返回('an'=>array(牌)或为空, 'ming'=>pai或false)
         */
        function checkMoPaiGang($playerPai, $playerPeng, $pai){
            $result = array();
            $result['an'] = $this->checkAnGang($playerPai, $pai);
            $result['ming'] = $this->checkMingGang($playerPai, $playerPeng, $pai);
            //   print_r($playerPeng);
            return $result;
        }


        /**
         * 检查玩家摸牌后是否能够暗杠
         * @param $playerPai 玩家初始牌
         * @param $pai 玩家摸到的牌
         * @return array 返回可以暗杠的数组
         */
        private  function checkAnGang($playerPai, $pai){
            array_push($playerPai, $pai);
            sort($playerPai);
            $pai_count = array_count_values($playerPai);
            $result = array();
            foreach($pai_count as $key => $value){
                if($value == 4){
                    array_push($result, $key);
                }
            }
            return $result;
        }


        /**
         * 检查玩家摸牌后是否能够明杠
         * @param $playerPai 玩家手牌
         * @param $playerPengPai 玩家碰的牌
         * @return 如果有，返回该牌，否则返回false;
         */
        private function checkMingGang($playerPai, $playerPengPai, $pai){
            //如果摸得牌可以杠
            for($i = 0; $i < count($playerPengPai); $i++){
                if($playerPengPai[$i] == $pai){
                    return array($pai);
                }
            }
            //否则，还要检查手中的牌是否可以杠
            for($i = 0; $i < count($playerPengPai); $i++){
                if(in_array($playerPengPai[$i], $playerPai)){
                    return array($playerPengPai[$i]);
                }
            }

            return array();
        }

        /**
         * 处理玩家手牌的函数，将打过来的牌或者摸得牌放入数组并排序
         * @param $playerPai，玩家的手牌
         * @param $pai，玩家摸到或者打给玩家的牌
         * @return array
         */
        private function processPlayerPai($playerPai, $pai){
            if($pai)
                array_push($playerPai, $pai);
            sort($playerPai);
            return $playerPai;
        }

        /**
         * 检查能否胡牌的函数
         * @param $playerPai,玩家当前的手牌
         * @param $pai，玩家摸到或者打给玩家的牌
         * @return false / true
         */
        function checkHu($playerPai, $pai){
            $huPaiCheck = $this->processPlayerPai($playerPai, $pai);
            //普通胡牌检测

            $r =  $this->doCheckHu($huPaiCheck);
            // print_r($huPaiCheck);

            if(!$r)
                //七对胡牌检测
                $r = $this->canSevenHu($huPaiCheck);

            if($r)
                return array($pai);
            else
                return array();

        }

        /**
         * 辅助checkHu检查胡牌的函数
         * @param $checkArray  由checkHu组合的字符串
         * @return bool 返回是否能胡牌
         */
        private function doCheckHu($checkArray){
            //如果只有两张牌
            if(count($checkArray) == 2){
                return $checkArray[0] == $checkArray[1];
            }
            $canHu = false;
            //找出将牌
            for($i = 0; $i < count($checkArray) - 1; $i++){
                //如果当前牌和后一张一样，则可为将
                if($checkArray[$i] == $checkArray[$i+1]){
                    //如果将牌所在位置位于倒数第二个位置，直接截取前面所有的牌作为判断
                    if($i == count($checkArray) - 2){
                        $tempArray = array_slice($checkArray, 0, $i);
                    }
                    //如果将牌在一开始的位置，只截取后面的牌作为判断
                    else if($i == 0){
                        $tempArray = array_slice($checkArray, $i+2, count($checkArray) - 2);
                    }
                    //否则，从0到将牌所在位置，并从跳过将牌的位置到最后分别截取数组，合并成一个判断数组
                    else{
                        $tempArray1 = array_slice($checkArray, $i+2, count($checkArray) - $i - 2);

                        $tempArray2 = array_slice($checkArray, 0, $i);
                        $tempArray = array_merge($tempArray1, $tempArray2);
                        sort($tempArray);
                    }

                    //判断剩余牌组是否符合AAA 或ABC组合，则可以得出能否胡牌
                    if($this->canHu($tempArray)){
                        return true;
                    }

                }
            }
            return false;
        }

        /**
         * 辅助doCheckHu检查胡牌的函数
         * @param $checkPais  由doCheckHu分割形成的除了将牌的手牌
         * @return bool 返回是否能胡牌
         */
        private function canHu($checkPais){
            //如果截取的牌组为0,则已经可以胡牌了，这种情况不会发生
            if(count($checkPais) == 0){
                return true;
            }
            //保存一个临时数组，用于处理
            $tempPais = $checkPais;

            if($this->_canHuFirstCan($tempPais)||$this->_canHuFirstShun($tempPais)){
                return true;
            }else{
                return false;
            }



        }

        private function _canHuFirstShun($checkPais){
            $tempPais = $checkPais;
            for($i = 0; $i < count($checkPais); $i++){
                //移除顺子
                $this->removeShun($tempPais, $checkPais[$i]);
                //如果一张牌不剩，则能胡
                if(count($tempPais) == 0){
                    return true;
                }
                //如果剩余牌小于3，则不能胡
                if(count($tempPais) < 3){
                    return false;
                }
            }


            //如果删除砍以后，只有三张以下的牌，不能胡
            if(count($tempPais) < 3){
                return false;
            }

            //判断能否形成顺子
            //将删除完坎子的数组，保存一份备份
            $checkPais = $tempPais;



            //得到数组中所有元素出现的次数
            $count = array_count_values($checkPais);
            foreach($count as $key => $value){
                //如果次数大于3
                if($value >= 3){
                    //则删除次数对应的KEY对应的砍牌
                    $this->removeKan($tempPais, $key);
                }
            }
            //删除砍以后，如果没有牌，则能胡
            if(count($tempPais) == 0){
                return true;
            }

            return false;
        }

        private function _canHuFirstCan($checkPais){
            $tempPais = $checkPais;
            //得到数组中所有元素出现的次数
            $count = array_count_values($checkPais);
            foreach($count as $key => $value){
                //如果次数大于3
                if($value >= 3){
                    //则删除次数对应的KEY对应的砍牌
                    $this->removeKan($tempPais, $key);
                }
            }


            //如果删除砍以后，只有三张以下的牌，不能胡
            if(count($tempPais) < 3){
                return false;
            }

            //判断能否形成顺子
            //将删除完坎子的数组，保存一份备份
            $checkPais = $tempPais;


            for($i = 0; $i < count($checkPais); $i++){
                //移除顺子
                $this->removeShun($tempPais, $checkPais[$i]);
                //如果一张牌不剩，则能胡
                if(count($tempPais) == 0){
                    return true;
                }
                //如果剩余牌小于3，则不能胡
                if(count($tempPais) < 3){
                    return false;
                }
            }


            //删除砍以后，如果没有牌，则能胡
            if(count($tempPais) == 0){
                return true;
            }

            return false;
        }

        /**
         * 辅助函数，用于删除牌中的一砍牌
         * @param $pais
         * @param $value
         */
        private function removeKan(&$pais, $value){
            for($i = 0; $i < count($pais); $i++){
                if($pais[$i] == $value){
                    array_splice($pais, $i, 3);
                    break;
                }
            }
        }

        /**
         * 辅助函数，用于删除牌中的顺子牌
         * @param $pais
         * @param $value
         */
        private function removeShun(&$pais, $value){
            //先找第一张牌
            for($pai1Start = 0; $pai1Start < count($pais); $pai1Start++){
                //如果找到第一张牌
                if($pais[$pai1Start] == $value){
                    //从找到第一张牌开始后面的位置，找第二张牌
                    for($pai2Start = $pai1Start+1; $pai2Start < count($pais); $pai2Start++){
                        //如果找到第二张牌
                        if($pais[$pai2Start] == $value + 1){
                            //从找到第二张牌开始后面的位置，找第三张牌
                            for($pai3Start = $pai2Start + 1; $pai3Start < count($pais); $pai3Start++){
                                //如果找到第三张牌
                                if($pais[$pai3Start] == $value + 2){
                                    //删除所有数据
                                    array_splice($pais, $pai3Start, 1);
                                    array_splice($pais, $pai2Start, 1);
                                    array_splice($pais, $pai1Start, 1);
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }

        /**
         * 检查七小队/龙七对能胡的函数
         * @param $checkPai
         * @return bool true or false表示能否胡牌
         */
        private function canSevenHu($checkPai){
            if(count($checkPai) != 14){
                return false;
            }

            $pairCount = 0;
            for($i = 0; $i < count($checkPai); $i += 2){
                if($checkPai[$i] == $checkPai[$i+1]){
                    $pairCount++;
                }
            }
            return $pairCount == 7;
        }

        /**
         * 检查红五胡的函数
         * @param $checkPai
         * @return bool true or false表示能否胡牌
         */
        private function checkHongWu($checkPai,$pai){
            if($pai==5||$pai==15||$pai==25){
                $tempArray=array_count_values($checkPai);//得到每个值出现的次数

                $fourNum=0;
                $sixNum=0;
                if(!empty($tempArray[$pai-1])){
                    $fourNum=$tempArray[$pai-1];
                }
                if(!empty($tempArray[$pai+1])){
                    $sixNum=$tempArray[$pai+1];
                }

                if(($fourNum==1||$fourNum==3||$fourNum==4)&&($sixNum==1||$sixNum==3||$sixNum==4)){
                    echo "MaJongLogic-checkHongWu OK!\n";
                    return true;
                }

                return false;
            }else{
                return false;
            }
        }

        /**
         * 检查是否能够推牌的函数，其实就是查叫，看看当玩家摸到一张牌后，打出任意一张牌后，是否能听牌
         * @param $playerPai 玩家的手牌
         * @param $pai 玩家摸到的牌
         * @return array  (i=>array(打的什么牌, array(a, b, c)具体胡的牌)
         */
        function canTingPai($playerPai, $pai){
            $tingArray = array();
            $checkTingPai = $this->processPlayerPai($playerPai, $pai);

            //基本思路，假设打出任意一张牌，且加入任意一张牌能胡牌，那这张牌就可以听牌
            for($i = 0; $i < count($checkTingPai); $i++){
                //如果已经判断过这张牌，跳过
                if($this->isTingPai($tingArray, $checkTingPai[$i])){
                    continue;
                }
                //保存现有牌组
                $tempArray = $checkTingPai;
                //按照顺序，从牌组中移除第i张牌，相当于打了这张牌
                array_splice($tempArray, $i, 1);
                //检查能否听牌
                $r = $this->doCheckTingPai($tempArray);

                if(count($r) > 0){
                    array_push($tingArray, array($checkTingPai[$i], $r));
                }
            }
            return $tingArray;
        }

        /**
         * 辅助函数，用于判断当前这张牌是否判断过听牌
         * @param $tingPaiArray 已经听牌的结果
         * @param $pai 判断的牌
         * @return bool TRUE/FALSE
         */
        private function isTingPai($tingPaiArray, $pai){
            for($i = 0; $i < count($tingPaiArray); $i++){
                if($tingPaiArray[$i][0] == $pai){
                    return true;
                }
            }
            return false;
        }

        /**
         * 实际检测听牌的函数，基本思路为遍历所有牌型
         * @param $processedPai
         * @return array
         */
        public function doCheckTingPai($processedPai){
            //记录能胡牌的数组，也就是在打出某张牌后能听的牌
            $result = array();
            //便利所有牌
            for($i = 0; $i < 3; $i++){
                for($j = 1; $j <= 9; $j++){
                    //先保存现有牌型
                    $tempArray = $processedPai;
                    //压入任意牌型
                    array_push($tempArray, $i * 10 + $j);
                    //排序
                    sort($tempArray);
                    //检查当前牌组能否胡牌
                    $r = $this->doCheckHu($tempArray);

                    //如果可以，则为所听的牌
                    if($r){
                        array_push($result, $i*10+$j);
                    }
                    //检查特殊牌型能不能胡牌
                    $r = $this->canSevenHu($tempArray);
                    if($r)
                        array_push($result, $i * 10 + $j);
                }
            }
            return $result;
        }

        /**
         * 获取胡牌情况的函数，
         * @param $playerPai 玩家手牌
         * @param $pengPai 玩家碰的牌
         * @param $gangPai 玩家杠的牌
         * @return array(QING_YI_SE=> 0/1, DUI_ZI_HU=>0/1, JIANG_DUI=>0/1, 'GOU'=>int, 'QUE'=>0/1)
         */
        public function getHuInfo($playerPai, $pengPai, $gangPai,$pai){

            $tempPai = array($playerPai, $pengPai, $gangPai);
            $returnArray = array(
                QING_YI_SE => $this->checkQing($tempPai),
                DUI_ZI_HU => $this->checkDuiZiHu($tempPai),
                JIANG_DUI => $this->checkJiangDui($tempPai),
                AN_QI_DUI => $this->canSevenHu($playerPai),
                HONG_WU => $this->checkHongWu($playerPai,$pai),
                'yaoJiuDui'=>$this->checkYaoJiuDui($tempPai),
                'GOU' => $this->checkGou($tempPai),
                'QUE' => $this->isQue($tempPai),
            );
            return $returnArray;
        }

        /**
         * 检查缺的函数
         * @param $pai，打包过的牌，[0]玩家手牌，[1]碰牌,[2]杠牌
         * @return int 1/0
         */
        function isQue($pai){
            $hasTong = false;
            $hasWan = false;
            $hasTiao = false;
            $temp = array_merge($pai[0], $pai[1], $pai[2]);
            sort($temp);
            for($i = 0; $i < count($temp); $i++){
                if($temp[$i] > 0 && $temp[$i] <= 10){
                    $hasTong = true;
                }
                if($temp[$i] >= 11 && $temp[$i] < 20){
                    $hasTiao = true;
                }
                if($temp[$i] >= 21 && $temp[$i] < 30){
                    $hasWan = true;
                }
            }
            return !($hasWan && $hasTong && $hasTiao);
        }

        /**
         * 检查杠牌加翻的情况
         * @param $tempPai
         * @return int
         */
        public function checkGou($tempPai){
            //首先得到手牌的个数，如果有手牌个数为4，即有杠加翻情况
            $hand_count = array_count_values($tempPai[0]);
            $gouCount = 0;
            foreach($hand_count as $pai => $paiCount){
                if($paiCount == 4){
                    $gouCount++;
                }
            }

            //然后检查碰牌，是否有勾的情况,检查方法为碰的牌在手牌中出现的牌
            for($i = 0; $i < count($tempPai[1]); $i++){
                foreach($hand_count as $pai => $paiCount){
                    if($paiCount == 1 && $pai == $tempPai[1][$i]){
                        $gouCount++;
                    }
                }
            }
            //最后看杠的牌的情况
            return $gouCount + count($tempPai[2]);
        }

        /**
         * 检查是否出现2、5、8将对胡的情况
         * @param $tempPai, 打包过的牌
         * @return int 0/1
         */
        public function checkJiangDui($tempPai){
            //先检查是否为对子胡
            $r = $this->checkDuiZiHu($tempPai);
            if($r != DUI_ZI_HU){
                return 0;
            }
            //如果是对子胡
            else{
                //检查手牌是否都为将牌
                $handPai = $tempPai[0];
                for($i = 0; $i < count($handPai); $i++){
                    if(!$this->checkJiangPai($handPai[$i])){
                        return 0;
                    }
                }
            }

            //检查玩家碰过的牌是否都为将牌
            for($i = 0; $i < count($tempPai[2]); $i++){
                if(!$this->checkJiangPai($tempPai[2][$i]))
                    return 0;
            }

            //检查玩家杠过的牌是否都为将牌
            for($i = 0; $i < count($tempPai[3]); $i++){
                if(!$this->checkJiangPai($tempPai[3][$i])){
                    return 0;
                }
            }
            return 1;

        }

        /**
         * 检查是否出现1、9对胡的情况
         * @param $tempPai, 打包过的牌
         * @return int 0/1
         */
        public function checkYaoJiuDui($tempPai){
            //先检查是否为对子胡
            $r = $this->checkDuiZiHu($tempPai);
            if($r != DUI_ZI_HU){
                return 0;
            }
            //如果是对子胡
            else{
                //检查手牌是否都为将牌
                $handPai = $tempPai[0];
                for($i = 0; $i < count($handPai); $i++){
                    if(!$this->checkYaoJiuPai($handPai[$i])){
                        return 0;
                    }
                }
            }

            //检查玩家碰过的牌是否都为将牌
            for($i = 0; $i < count($tempPai[2]); $i++){
                if(!$this->checkYaoJiuPai($tempPai[2][$i]))
                    return 0;
            }

            //检查玩家杠过的牌是否都为将牌
            for($i = 0; $i < count($tempPai[3]); $i++){
                if(!$this->checkYaoJiuPai($tempPai[3][$i])){
                    return 0;
                }
            }
            return 1;
        }

        /**
         * 辅助函数，检查某张牌是否为1、9将牌
         * @param $pai
         * @return bool
         */
        public function checkYaoJiuPai($pai){
            $reminder = $pai % 10;
            if($reminder != 1 || $reminder != 9){
                return false;
            }
            return true;
        }

        /**
         * 辅助函数，检查某张牌是否为2、5、8将牌
         * @param $pai
         * @return bool
         */
        public function checkJiangPai($pai){
            $reminder = $pai % 10;
            if($reminder != 2 || $reminder != 5 || $reminder != 8){
                return false;
            }
            return true;
        }

        /**
         * 检查是否为清一色的函数
         * @param $tempPai
         * @return int
         */
        public function checkQing($tempPai){
            // print_r($tempPai);
            //做一个类型的循环
            for($type = 1; $type <= 3; $type++){
                $r = $this->doCheckQing($tempPai, $type);
                //如果某一种类型检查出为清一色,则返回结果，否则继续检查
                if($r){
                    return 1;
                }
            }
            //最后则不为清一色
            return 0;
        }

        /**
         * 实际检查是否为清一色的辅助函数
         * @param $tempPai,打包后的牌
         * @param $type，要检查的类型：1、筒子，2、条子，3、万子
         * @return bool
         */
        public function doCheckQing($tempPai, $type){
            //分别检查手牌，碰的牌，杠的牌
            for($i = 0; $i < 3; $i++){
                $paiArray = $tempPai[$i];
                for($j = 0; $j < count($paiArray); $j++){
                    //取牌的取整结果
                    $reminder = ceil($paiArray[$j] / 10);
                    if($reminder != $type){
                        return false;
                    }
                }
            }
            return true;
        }

        /**
         * 检查是否为对子胡的函数
         * 对子胡理论上的样式为：AA + n * BBB
         * @param $tempPai
         * @return bool
         */
        public function checkDuiZiHu($tempPai){
            $playerPai = $tempPai[0];
            $firstJiang = false;
            $pai_count = array_count_values($playerPai);
            foreach($pai_count as $pai => $count){
                //如果某个牌的数量<2，不可能是对子胡
                if($count < 2){
                    return 0;
                }
                //如果有牌的数量为2
                if($count == 2){
                    //且是唯一一个，则还能继续判断
                    if(!$firstJiang){
                        $firstJiang = true;
                    }
                    //否则，不可能是对子胡
                    else{
                        return 0;
                    }
                }
                //如果手中的牌有4张一样的，也不可能是对子胡
                if($count == 4){
                    return 0;
                }
            }
            return 1;
        }

        /**
         * 检查能否胡龙七对的函数
         * @param $tempPai:打包过的牌
         * @return bool
         */
        public function checkLongQiDui($tempPai){
            //先检查是否为七小队牌型
            $r = $this->canSevenHu($tempPai[0]);
            //如果是
            if($r){
                //检查是否有两个勾，如果有，则为龙七对
                $pai_count = array_count_values($tempPai[0]);
                $gouCount = 0;
                foreach($pai_count as $pai => $count){
                    if($count == 4){
                        $gouCount++;
                    }
                }
                if($gouCount == 2){
                    return true;
                }
                else{
                    return false;
                }
            }
            else{
                return false;
            }
        }

    }


?>